<?php
declare (strict_types = 1);

namespace app\adminapi\middleware;

use app\adminapi\extend\tools\jwt\JWT;
use think\exception\HttpException;
use think\facade\Cache;

/**
 * 后台管理接口安全
 * 1. token(凭证) 登录成功之后颁发的认证、验证token (需要登录成功之后需要访问接口)
 * 2.时间戳机制 （一个请求从客户端到服务端请求时间、进行检测）
 * 3.请求频率限制（限制的路由、也就是控制器/方法）
 * 4.权限检测（检测用户是否有权限访问、检测控制器/方法）
 * 5.签名机制（把请求参数安装一定规则进行排序、处理、加密生成的一个字符串）请求数据（例如：添加商品 goods_name）
 * （客户端生成一个签名、服务端也生成一个签名、两个进行对不）如果签名不一致、返回请求失败、签名一致、请求才有效
 * 备注：每次请求的签名是不一致的
 *
 * 将验证优化至自定义面类、进行调用
 * 写在中间可以
 * 通过门面实现静态调用（Facade功能可以让类无需实例化而直接进行静态方式调用）
 * 为什么要拿出去？
 * Class Login
 * @package app\adminapi\middleware
 */
class Login
{
    /**
     * 处理请求
     *
     * @param \think\Request $request
     * @param \Closure       $next
     * @return Response
     */
    public function handle($request, \Closure $next)
    {
        # 获取客户端传送过来的token、进行验证
        # token机制保证接口安全
        try {
//            $token = JWT::getRequestToken();
//            $user_id = JWT::getUserId($token);// 用户id、有些接口不需要登录、ip
//            if (!$user_id) {
//                abort(2001,"请先登录");
//            }

            $user_id = \app\adminapi\facade\Check::getUserId();
            return $next($request);
            // 权限检测、属于后台管理
            # 方法体内抛出异常信息就可以
            $this->checkAuth($user_id);

            # 用户已经登录
            # 时间戳机制
            # 第一步 、将中间中的方法、单独封装、另外一个类
//            $this->checkRequestTime();
            # 正常调用类中的一个方法
//            $checkObj = new \app\adminapi\common\Check();
//            $checkObj->checkRequestTime();

            # 我想通过门面去实现

            \app\adminapi\facade\Check::checkRequestTime();

            # 签名的机制
            $this->checkSign();

            # 接口访问次数的一个限制
            # 请求过于频繁，请稍后再试
            $this->checkRequestSeconds();//请求次数限制

            return $next($request);
        }catch (HttpException $httpException) {
            // 在catch里面就可以统一处理
//            dd($httpException->getMessage());
            return  fail($httpException->getStatusCode(),$httpException->getMessage());
        }

    }

    /**
     * 请求次数限制
     * 1分钟之内之内访问3次
     * 限制ip、限制的范围比较大、怎么细化限制（限制到某个路由、权限）
     * 所有的路由 1分钟之内之内访问3次（不合理、怎么去优化、商品列表、商品添加、商品修改）
     */
    public function checkRequestSeconds(){
        // 范围比较大
        $client_ip = request()->ip();# 助手函数、本地 127.0.0.1
        $client_key = md5($client_ip.'throttle');

        # 限制的是接口 实际上就是访问的控制器、方法
        $route = request()->controller().request()->action();
        $client_key = md5($route.'throttle');

        # 保存请求次数
        # 获取当前ip是否访问过
        $is_request = cache($client_key);
        # 验证请求次数
        if ($is_request >3) {
           abort(4003,"请求过于频繁，请稍后再试");
        }

        if (empty($is_request)) {
            # 没有请求过 tp封装缓存机制、也可以修改为redis
            Cache::set($client_key,1,60);// 60过期时间
        } else {
            # 请求过
            Cache::inc($client_key);

        }

    }

    /**
     * 自己实现的
     * 检测请求时间是否超时
     */
    protected function checkRequestTime(){

        $client_time = request()->all('timestamp');//客户端的时间

        $serve_time = time();// 服务器的时间
        # 超过3分钟属于非法请求
        if (empty($client_time) || $serve_time - $client_time > 86400 ) {
            abort(2001,"请求超时、请稍后重试");
        }

    }

    /**
     * 权限检测
     * @param $user_id
     * @return bool
     * @throws \Exception
     */
    protected function checkAuth($user_id){
        $cache_auth = \cache('userAuth');
        $current_auth = strtolower(request()->controller()).'/'.request()->action();

        if (!in_array($current_auth,$cache_auth)) {
            # 代码执行到这个、出现异常信息
            abort(4003,"无权限访问");//手动抛出异常
        }

    }

    /**
     * 检验签名
     * 1.0 （梳理签名流程）
     * 2.0(待开发的)
     */
    protected function checkSign(){
        $client_sign = request()->all('sign');
        $serve_sign = md5("goods_name=商品名称goods_price=6888");
        if ($client_sign!=$serve_sign) {
            abort(2001,'签名错误');
        }
    }
}
